package mpeg.audio;

import haxe.io.Bytes;
import haxe.io.Eof;
import haxe.io.Input;

class MpegAudioReader
{
	// The theoretical absolute maximum frame size is 2881 bytes
	// (MPEG 2.5 Layer II 160Kb/s, with a padding slot).
	//
	// This is the next-largest power-of-two.
	static inline var BUFFER_SIZE = 4096;
	static inline var HEADER_SIZE = 4;
	static inline var CRC_SIZE = 4;
	static var infoTagSignature = Bytes.ofString("Info");
	static var xingTagSignature = Bytes.ofString("Xing");
	static var versions = [MpegVersion.Version25, null, MpegVersion.Version2, MpegVersion.Version1];
	static var layers = [null, Layer.Layer3, Layer.Layer2, Layer.Layer1];
	static var version1Bitrates = [
		[
			null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null
		],
		[
			0, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, null
		],
		[
			0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, 384000, null
		],
		[
			0, 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, 416000, 448000, null
		]
	];
	static var version2Bitrates = [
		[
			null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null
		],
		[
			0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, null
		],
		[
			0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, null
		],
		[
			0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, 224000, 256000, null
		]
	];
	static var samplingFrequenciesByVersionIndex = [
		[11025, 12000, 8000, null],
		[null, null, null, null],
		[22050, 24000, 16000, null],
		[44100, 48000, 32000, null]
	];
	static var modes = [Mode.Stereo, Mode.JointStereo, Mode.DualChannel, Mode.SingleChannel];
	static var emphases = [Emphasis.None, Emphasis.RedBook, null, Emphasis.J17];
	static var slotSizeByLayerIndex = [0, 1, 1, 4];
	static var slotsPerBitPerSampleByLayerIndexByVersionIndex = [[null, 72, 144, 12], null, [null, 72, 144, 12], [null, 144, 144, 12]];

	var input:Input;
	var state:MpegAudioReaderState;
	var seenFirstFrame:Bool;
	var buffer:Bytes;
	var bufferCursor:Int;
	var bufferLength:Int;

	public function new(input:Input)
	{
		if (input == null)
		{
			throw "input must not be null";
		}

		this.input = input;
		this.state = MpegAudioReaderState.Start;

		seenFirstFrame = false;

		buffer = Bytes.alloc(BUFFER_SIZE);
		bufferCursor = 0;
		bufferLength = 0;
	}

	public function readAll()
	{
		if (state != MpegAudioReaderState.Start)
		{
			throw "Cannot combine calls to readNext and readAll";
		}

		var frames:Array<Frame> = [];

		var encoderDelay:Int = 0;
		var endPadding:Int = 0;

		while (true)
		{
			var element = readNext();

			switch (element)
			{
				case Frame(frame):
					frames.push(frame);

				case Info(_):
				// Discard info tag.

				case GaplessInfo(giEncoderDelay, giEndPadding):
					encoderDelay = giEncoderDelay;
					endPadding = giEndPadding;

				case Unknown(_):
				// Discard unknown bytes

				case End:
					break;
			}
		}

		var audio = new MpegAudio(frames, encoderDelay, endPadding);

		return audio;
	}

	public function readNext()
	{
		switch (state)
		{
			case Start, Seeking:
				return seek();

			case Info(info):
				return infoTagGaplessInfo(info);

			case Frame:
				return frame();

			case End:
				return end();

			case Ended:
				throw new Eof();
		}
	}

	function seek()
	{
		bufferCursor = 0;

		try
		{
			do
			{
				do
				{
					if (!bufferSpace(2))
					{
						return yieldUnknown();
					}
				}
				while (readByte() != 0xff);
			}
			while ((readByte() & 0x80) != 0x80);
		}
		catch (eof:Eof)
		{
			return end();
		}

		if (bufferCursor > 2)
		{
			state = MpegAudioReaderState.Frame;
			return yieldUnknown(bufferCursor - 2);
		}
		else
		{
			return frame();
		}
	}

	function frame()
	{
		var b:Int;
		try
		{
			b = readByte(1);
		}
		catch (eof:Eof)
		{
			return end();
		}
		var versionIndex = (b >> 3) & 0x3;
		var layerIndex = (b >> 1) & 0x3;
		var hasCrc = b & 1 == 0;

		try
		{
			b = readByte(2);
		}
		catch (eof:Eof)
		{
			return end();
		}
		var bitrateIndex = (b >> 4) & 0xf;
		var samplingFrequencyIndex = (b >> 2) & 0x3;
		var hasPadding = (b >> 1) & 1 == 1;
		var privateBit = b & 1 == 1;

		try
		{
			b = readByte(3);
		}
		catch (eof:Eof)
		{
			return end();
		}
		var modeIndex = (b >> 6) & 0x3;
		var modeExtension = (b >> 4) & 0x3;
		var copyright = (b >> 3) & 1 == 1;
		var original = (b >> 2) & 1 == 1;
		var emphasisIndex = b & 0x3;

		var version = versions[versionIndex];
		var layer = layers[layerIndex];
		var bitrate = switch (version)
		{
			case Version1: version1Bitrates[layerIndex][bitrateIndex];
			case Version2, Version25: version2Bitrates[layerIndex][bitrateIndex];
		}
		var samplingFrequency = samplingFrequenciesByVersionIndex[versionIndex][samplingFrequencyIndex];
		var mode = modes[modeIndex];
		var emphasis = emphases[emphasisIndex];

		if (version == null || layer == null || bitrate == null || samplingFrequency == null || emphasis == null)
		{
			// This isn't a valid frame.
			// Seek for another frame starting from the byte after the bogus syncword.
			state = MpegAudioReaderState.Seeking;
			return yieldUnknown(1);
		}

		var frameData:Bytes;

		if (bitrate == 0)
		{
			// free-format bitrate

			var end = false;
			try
			{
				do
				{
					do
					{
						if (!bufferSpace(2))
						{
							return yieldUnknown();
						}
					}
					while (readByte() != 0xff);
				}
				while ((readByte() & 0xf8) != 0xf8);
			}
			catch (eof:Eof)
			{
				end = true;
			}

			var frameLengthBytes = if (end) bufferCursor else bufferCursor - 2;
			frameLengthBytes -= (frameLengthBytes % slotSizeByLayerIndex[layerIndex]);

			var frameLengthSlots = Math.floor(frameLengthBytes / slotSizeByLayerIndex[layerIndex]);

			bitrate = Math
				.floor(samplingFrequency * frameLengthSlots / slotsPerBitPerSampleByLayerIndexByVersionIndex[versionIndex][layerIndex]); // TODO should bitrate be Float?

			frameData = yieldBytes(frameLengthBytes);
		}
		else
		{
			var frameLengthSlots = Math.floor(slotsPerBitPerSampleByLayerIndexByVersionIndex[versionIndex][layerIndex] * bitrate / samplingFrequency);

			if (hasPadding)
			{
				frameLengthSlots += 1;
			}

			var frameLengthBytes = frameLengthSlots * slotSizeByLayerIndex[layerIndex];

			try
			{
				readBytesTo(frameLengthBytes - 1);
			}
			catch (eof:Eof)
			{
				return end();
			}

			frameData = yieldBytes();
		}

		var header = new FrameHeader(version, layer, hasCrc, bitrate, samplingFrequency, hasPadding, privateBit, mode, modeExtension, copyright, original,
			emphasis);

		if (!seenFirstFrame)
		{
			seenFirstFrame = true;

			if (layer == Layer.Layer3)
			{
				var info = readInfo(header, frameData);
				if (info != null)
				{
					state = MpegAudioReaderState.Info(info);
					return Element.Info(info);
				}
			}
		}

		var frame = new Frame(header, frameData);

		state = MpegAudioReaderState.Seeking;
		return Element.Frame(frame);
	}

	function readInfo(header:FrameHeader, frameData:Bytes)
	{
		var sideInformationSize = switch (header.version)
		{
			case Version1: switch (header.mode)
				{
					case Stereo, JointStereo, DualChannel: 32;
					case SingleChannel: 17;
				};
			case Version2, Version25: switch (header.mode)
				{
					case Stereo, JointStereo, DualChannel: 17;
					case SingleChannel: 9;
				}
		};

		var sideInformationStartIndex = HEADER_SIZE + (if (header.hasCrc) CRC_SIZE else 0);

		var infoStartIndex = sideInformationStartIndex + sideInformationSize;

		for (i in sideInformationStartIndex...infoStartIndex)
		{
			if (frameData.get(i) != 0)
			{
				return null;
			}
		}

		if (frameData.sub(infoStartIndex, infoTagSignature.length).compare(infoTagSignature) == 0 || frameData.sub(infoStartIndex, xingTagSignature.length)
			.compare(xingTagSignature) == 0)
		{
			return new Info(header, infoStartIndex, frameData);
		}
		else
		{
			return null;
		}
	}

	function infoTagGaplessInfo(info:Info)
	{
		var b0 = info.frameData.get(info.infoStartIndex + 0x8d);
		var b1 = info.frameData.get(info.infoStartIndex + 0x8e);
		var b2 = info.frameData.get(info.infoStartIndex + 0x8f);

		var encoderDelay = ((b0 << 4) & 0xff0) | ((b1 >> 4) & 0xf);
		var endPadding = ((b1 << 8) & 0xf00) | (b2 & 0xff);

		state = MpegAudioReaderState.Seeking;
		return Element.GaplessInfo(encoderDelay, endPadding);
	}

	function end()
	{
		var unknownElement = yieldUnknown(bufferLength);

		if (unknownElement == null)
		{
			state = MpegAudioReaderState.Ended;
			return Element.End;
		}
		else
		{
			state = MpegAudioReaderState.End;
			return unknownElement;
		}
	}

	function yieldUnknown(length = -1)
	{
		if (length == -1)
		{
			length = bufferCursor;
		}

		if (length == 0)
		{
			return null;
		}

		return Element.Unknown(yieldBytes(length));
	}

	function yieldBytes(length = -1)
	{
		if (length == -1)
		{
			length = bufferCursor;
		}
		else if (length == 0)
		{
			return Bytes.alloc(0);
		}

		assert(length > 0 && length <= bufferLength);

		var bytes:Bytes = Bytes.alloc(length);
		bytes.blit(0, buffer, 0, length);

		buffer.blit(0, buffer, length, bufferLength - length);

		bufferLength -= length;
		bufferCursor -= length;

		return bytes;
	}

	inline function assert(condition:Bool)
	{
		if (!condition)
		{
			throw "MpegAudioReader internal error";
		}
	}

	inline function bufferSpace(bytes = 1)
	{
		return bufferCursor + bytes <= BUFFER_SIZE;
	}

	inline function readByte(position:Int = -1)
	{
		if (position == -1)
		{
			position = bufferCursor;
		}

		readBytesTo(position);

		return buffer.get(position);
	}

	inline function readBytes(count:Int)
	{
		readBytesTo(bufferCursor + count);
	}

	inline function readBytesTo(position:Int)
	{
		assert(position >= 0 && position < BUFFER_SIZE);

		while (bufferLength <= position)
		{
			buffer.set(bufferLength, input.readByte());
			bufferCursor = ++bufferLength;
		}

		bufferCursor = position + 1;
	}
}

private enum MpegAudioReaderState
{
	Start;
	Seeking;
	Frame;
	Info(info:Info);
	End;
	Ended;
}
